AppRunnerをCDKで構築してみた
LINE事業部の太田です。
App RunnerがVPCリソースへのアクセスが可能になったので、RDS + App RunnerをCDKで構築してみたいと思います。
今回作ったものは以下にあります。
ECRを作成
CDKでECRを作成します。
- app-runner-example-stack.ts
import * as cdk from '@aws-cdk/core' import * as ecr from '@aws-cdk/aws-ecr' export class AppRunnerExampleStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props) // ECR const repository = new ecr.Repository(this, 'AppRunnerExampleRepository') } }
上記をデプロイすると、ECRが作成されます。
Webサーバーを用意
簡単なWebサーバーをExpressを使って構築します。
- src/index.ts
import express from 'express' const app = express() app.use(express.json()) app.use(express.urlencoded({ extended: true })) app.get('/', (req: express.Request, res: express.Response) => { res.send('Hello World!') }) app.listen(3000, () => { console.log('Example app listening on port 3000!') })
- package.json
{ "name": "app-runner-example", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "ts-node src/index.ts" }, "dependencies": { "express": "4.17.2" }, "devDependencies": { "@tsconfig/node14": "1.0.1", "@types/express": "4.17.13", "@types/node": "17.0.17", "ts-node": "10.5.0", "typescript": "4.5.5" } }
- Dockerfile
FROM node:14-slim WORKDIR /usr/src/app COPY . . RUN yarn install --pure-lockfile --non-interactive EXPOSE 3000 CMD ["yarn", "start"]
以下のコマンドでビルドしてECRにプッシュします。
$ aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com $ docker build -t app-runner-example . $ docker tag app-runner-example ${REPOSITORY_URI} $ docker push ${REPOSITORY_URI}
App Runnerを作成
CDKでApp Runnerを作成します。
- app-runner-example-stack.ts
import { AppRunner } from './app-runner' export class AppRunnerExampleStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props) // ECR const repository = new ecr.Repository(this, 'AppRunnerExampleRepository') // AppRunner new AppRunner(this, 'AppRunner', { repository }) } }
- app-runner.ts
import * as cdk from '@aws-cdk/core' import * as iam from '@aws-cdk/aws-iam' import * as apprunner from '@aws-cdk/aws-apprunner' import * as ecr from '@aws-cdk/aws-ecr' interface AppRunnerProps { repository: ecr.Repository } export class AppRunner extends cdk.Construct { constructor(scope: cdk.Construct, id: string, props: AppRunnerProps) { super(scope, id) const { repository } = props // Roles const instanceRole = new iam.Role(scope, 'AppRunnerInstanceRole', { assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com'), }) const accessRole = new iam.Role(scope, 'AppRunnerAccessRole', { assumedBy: new iam.ServicePrincipal('build.apprunner.amazonaws.com'), }) // Apprunner new apprunner.Service(scope, 'AppRunnerExampleService', { source: apprunner.Source.fromEcr({ imageConfiguration: { port: 3000, // Webサーバーを3000ポートとしているので、ここを合わせます }, repository, tag: 'latest', }), instanceRole: instanceRole, accessRole: accessRole, }) } }
上記をデプロイすると、App Runnerが作成されます。
デフォルトドメインにアクセスするとレスポンスが返ってきます。
$ curl https://xxxxxxxxxx.ap-northeast-1.awsapprunner.com Hello World!
RDS・VPCを作成
App RunnerからRDSにアクセスできるようにしていきます。
RDSやVPCなどをCDKで作成します。
- app-runner-example-stack.ts
import * as cdk from '@aws-cdk/core' import * as ecr from '@aws-cdk/aws-ecr' import { AppRunner } from './app-runner' import { Network } from './network' import { Rds } from './rds' export class AppRunnerExampleStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props) // ECR const repository = new ecr.Repository(this, 'AppRunnerExampleRepository') // VPC const { vpc, dbSecurityGroup } = new Network(this, 'Network') // RDS new Rds(this, 'Rds', { vpc, dbSecurityGroup }) // AppRunner new AppRunner(this, 'AppRunner', { repository }) } }
- network.ts
import * as cdk from '@aws-cdk/core' import * as ec2 from '@aws-cdk/aws-ec2' export class Network extends cdk.Construct { readonly vpc: ec2.Vpc readonly dbSecurityGroup: ec2.SecurityGroup constructor(scope: cdk.Construct, id: string) { super(scope, id) this.vpc = new ec2.Vpc(this, 'VPC', { cidr: '10.0.0.0/16', subnetConfiguration: [ { cidrMask: 24, name: 'DB', subnetType: ec2.SubnetType.PRIVATE_ISOLATED, }, ], natGateways: 0, }) // App Runnerに設定するセキュリティグループ const AppRunnerSecurityGroup = new ec2.SecurityGroup( this, 'AppRunnerSecurityGroup', { securityGroupName: 'app-runner-example-sg-ar', vpc: this.vpc, } ) // RDSに設定するセキュリティグループ this.dbSecurityGroup = new ec2.SecurityGroup(this, 'DBSecurityGroup', { allowAllOutbound: true, securityGroupName: 'app-runner-example-sg-db', vpc: this.vpc, }) // AppRunnerSecurityGroupからのポート5432のインバウンドを許可 this.dbSecurityGroup.addIngressRule( AppRunnerSecurityGroup, ec2.Port.tcp(5432) ) } }
- rds.ts
import * as cdk from '@aws-cdk/core' import * as rds from '@aws-cdk/aws-rds' import * as ec2 from '@aws-cdk/aws-ec2' interface RdsProps { vpc: ec2.Vpc dbSecurityGroup: ec2.SecurityGroup } export class Rds extends cdk.Construct { constructor(scope: cdk.Construct, id: string, props: RdsProps) { super(scope, id) const { vpc, dbSecurityGroup } = props // RDSのパスワードを自動生成してSecret Managerに格納 const rdsCredentials = rds.Credentials.fromGeneratedSecret( 'appRunnerExample', { secretName: 'AppRunnerExampleDbSecret' } ) new rds.DatabaseCluster( scope, 'AppRunnerExampleDbCluster', { engine: rds.DatabaseClusterEngine.auroraPostgres({ version: rds.AuroraPostgresEngineVersion.VER_12_4, }), credentials: rdsCredentials, instances: 1, instanceProps: { instanceType: ec2.InstanceType.of( ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.MEDIUM ), vpc, vpcSubnets: vpc.selectSubnets({ subnetGroupName: 'DB' }), securityGroups: [dbSecurityGroup], }, defaultDatabaseName: 'appRunnerExample', } ) } }
- app-runner.ts
import * as cdk from '@aws-cdk/core' import * as iam from '@aws-cdk/aws-iam' import * as apprunner from '@aws-cdk/aws-apprunner' import * as ecr from '@aws-cdk/aws-ecr' import * as secretsmanager from '@aws-cdk/aws-secretsmanager' interface AppRunnerProps { repository: ecr.Repository } export class AppRunner extends cdk.Construct { constructor(scope: cdk.Construct, id: string, props: AppRunnerProps) { super(scope, id) ... // rds.tsで作成したRDSの接続情報をSecret Managerから取得 const secret = secretsmanager.Secret.fromSecretNameV2( scope, 'AppRunnerExampleDbSecret', 'AppRunnerExampleDbSecret' ) // Apprunner new apprunner.Service(scope, 'AppRunnerExampleService', { source: apprunner.Source.fromEcr({ imageConfiguration: { port: 3000, environment: { // DBの接続情報を環境変数へ格納 dbUserName: secret.secretValueFromJson('username').toString(), dbPassword: secret.secretValueFromJson('password').toString(), dbHost: secret.secretValueFromJson('host').toString(), dbPort: secret.secretValueFromJson('port').toString(), dbName: secret.secretValueFromJson('dbname').toString(), }, }, repository, tag: 'latest', }), instanceRole: instanceRole, accessRole: accessRole, }) } }
ここまでをデプロイすると、RDSやVCPが作成されます。
App RunnerからRDSに接続
App RunnerからRDSに接続するために設定をします。
CDKでできれば良いのですが、この辺りはまだCDKでは提供されてないようです。
App Runnerのコンソールに移動し、[設定]タブの[サービスを設定]の編集ボタンを押します。
編集画面になるので、ネットワークキングの設定で[カスタムVPC] - [新規追加]を押して以下のように設定します。
あとは、[追加] - [変更を保存]を押して終了です。
WebサーバーでRDSに接続
Webサーバー側でRDSに接続するように修正して、ECRにプッシュします。
ORMとしてprismaを利用します。
- src/index.ts
import express from 'express' import * as prisma from '@prisma/client' const app = express() const prismaClient = new prisma.PrismaClient() app.use(express.json()) app.use(express.urlencoded({ extended: true })) app.get('/users/:id', async (req: express.Request, res: express.Response) => { const id = req.params.id const user = await prismaClient.user.findUnique({ where: { id: Number(id) } }) res.send(user ?? `User not found`) }) app.post('/users', async (req: express.Request, res: express.Response) => { const name = req.body.name const user = await prismaClient.user.create({ data: { name, }, }) res.send(user) }) app.listen(3000, () => { console.log('Example app listening on port 3000!') })
- package.json
{ "name": "app-runner-example", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "ts-node src/index.ts", "db:generate": "prisma generate", "db:migrate": "prisma migrate deploy" }, "dependencies": { "@prisma/client": "3.9.2", "express": "4.17.2" }, "devDependencies": { "@tsconfig/node14": "1.0.1", "@types/express": "4.17.13", "@types/node": "17.0.17", "prisma": "3.9.2", "ts-node": "10.5.0", "typescript": "4.5.5" } }
- Dockerfile
FROM node:14-slim WORKDIR /usr/src/app # https://github.com/prisma/prisma/issues/1301#issuecomment-574317426 RUN apt-get -qy update RUN apt-get -qy install openssl COPY . . RUN yarn install --pure-lockfile --non-interactive RUN yarn db:generate EXPOSE 3000 CMD sh -c "yarn db:migrate && yarn start"
- prisma/schema.prisma
prismaのDBスキーム定義です。
generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) name String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
あとは再度、ビルド&プッシュしてApp Runnerを再度デプロイします。
$ aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com $ docker build -t app-runner-example . $ docker tag app-runner-example ${REPOSITORY_URI} $ docker push ${REPOSITORY_URI} $ aws apprunner start-deployment --service-arn ${APP_RUNNER_ARN}
App Runnerのデプロイが完了すれば、RDSにアクセス可能になります。
$ curl -X POST 'https://xxxxxxxxxx.ap-northeast-1.awsapprunner.com/users' -d 'name=test' { "id":1, "name":"test", "createdAt":"2022-02-15T07:31:23.638Z", "updatedAt":"2022-02-15T07:31:23.639Z" } $ curl https://xxxxxxxxxx.ap-northeast-1.awsapprunner.com/users/1 { "id":1, "name":"test", "createdAt":"2022-02-15T07:31:23.638Z", "updatedAt":"2022-02-15T07:31:23.639Z" }
感想
実際に利用するにあたり以下の内容が個人的には気になりました。
自動デプロイ
App Runnerでは指定したタグ(上記の例ではlatest)を持つイメージがプッシュされると自動的にデプロイされる機能がありますが、タグは完全一致みたいなのでコミットのハッシュなどからタグを生成した場合は利用はできないかと思います。
その場合は手動デプロイで対応することになると思うので、タグのプレフィックスなどで自動デプロイが出来ると更に便利なのかなと思いました。
また、現在だと自動デプロイの設定はCDK側からは出来ないのでコンソール側から設定する必要がありそうです。
WAFは未対応
現在だとWAFは未対応の状態です。
要望としてWAF対応のissueがあがっているので今後対応されるかもしれません。
VPCの接続設定が手動
CDKではまだVPCの接続設定ができないので、手動で設定を入れる必要があります。
また、CDKのデプロイでApp Runnerが更新されると設定が解除されてしまうのでそのあたりも注意が必要かもしれません。
App Runnerでイメージのタグを変えたい場合は、AWS CLIで更新する必要があります。
Secret Managerから環境変数展開
Secret Mangerからの環境変数はCDK側で展開して環境変数にいれているので、コンソールからRDSのパスワードが見れてしまいます。
ECSのように直接展開できると便利かと思いました。
最後に
ECSやfargateを利用するよりは遥かに簡単に環境が構築できました。
今回は無理にCDKを使った感があるので、現在App Runnerを利用するならコンソールもしくはAWS CLIからの構築でも良いかと思います。
今後も機能拡張されてさらに便利になると思うので、利用していきたいです。